﻿using System;
using System.Data;
using System.Data.SqlServerCe;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Globalization;

namespace VKontakteClient
{
    /// <summary>
    /// Расширитель строк для предотвращения случайных SQL-inj
    /// </summary>
    public static class StringExtender
    {
        /// <summary>
        /// Заменяет в строке кавычки на спецзнаки
        /// </summary>
        public static string EscapeString(this string nonEscapedValue)
        {
            return (nonEscapedValue??"").Replace("%27","%%27").Replace("'", "%27");
        }
        /// <summary>
        /// Заменяет в строке спецзнаки обратно на кавычки
        /// </summary>
        public static string UndoEscapeString(this string escapedValue)
        {
            return (escapedValue??"").Replace("%27", "'").Replace("%'","%27");
        }
    }

    /// <summary>
    /// Класс работы с БД
    /// </summary>
    public class SqlCEApi : IDisposable
    {
        ~SqlCEApi()
        {
            Dispose(true);
        }
        /// <summary>
        /// При удалении класса - закрытие соединения с БД
        /// </summary>
        public void Dispose()
        {
            GC.SuppressFinalize(this);
        }
        public void Dispose(bool closeNative)
        {
            if (_connection != null)
                if (_connection.State != ConnectionState.Closed)
                {
                    _connection.Close();
                }
        }

        /// <summary>
        /// Версия структуры БД
        /// </summary>
        static string DBVersion = "1.0.0.1";

        /// <summary>
        /// Конструктор класса
        /// </summary>
        public SqlCEApi()
        {
#if DEBUG
            DebugLog.Dump("SqlCeAPI()\r\n");
#endif
            _userTimeLoaded = new List<string>();
            _modifiesCache = new Dictionary<int, List<DateTime>>();
            _sqlLocalCache = new Dictionary<CombinedKey, object[]>();
            _userTimeCache = new Dictionary<TwoColumnKey<int, string>, DateTime?>();
            CheckDBVersion();
        }

        /// <summary>
        /// Метод проверки версии БД файла и собственной версии
        /// </summary>
        private void CheckDBVersion()
        {
#if DEBUG
            DebugLog.Dump("CheckDBVersion()\r\n");
#endif
            int i = 0;
            SqlCeCommand cmd;
            try
            {
                cmd = new SqlCeCommand("SELECT COUNT(*) FROM [SettingsTable] WHERE [key] LIKE 'DBVersion'", Connection);
                i = (int)(cmd.ExecuteScalar()??0);
#if DEBUG
                DebugLog.Dump("CheckDBVersion() -> checked for key\r\n");
#endif
            }
            catch (Exception e)
            {
                DebugLog.DebugException(e);
            }
            if (i == 0)
            {
#if DEBUG
                DebugLog.Dump("CheckDBVersion() -> key not found. inserting.\r\n");
#endif
                cmd = new SqlCeCommand("INSERT INTO [SettingsTable] ([key],[value]) VALUES ('DBVersion','" + DBVersion + "')", Connection);
                cmd.ExecuteNonQuery();
            }
            else
            {
#if DEBUG
                DebugLog.Dump("CheckDBVersion() -> key found. getting\r\n");
#endif
                cmd = new SqlCeCommand("SELECT value FROM [SettingsTable] WHERE [key] LIKE 'DBVersion'", Connection);
                string SavedVersion = (string)cmd.ExecuteScalar();
#if DEBUG
                DebugLog.Dump("CheckDBVersion() -> got key\r\n");
#endif
                if (SavedVersion != DBVersion)
                {
#if DEBUG
                    DebugLog.Dump("CheckDBVersion() -> key differs\r\n");
#endif
                    if (SavedVersion == "1.0.0.0") UpdateDBFrom1000To1001();
                    else if (SavedVersion == "1.0.0.1") UpdateDBFrom1001To1002();
                    else
                    {
                        MessageBox.Show("Версия БД(" + SavedVersion + ") не совпадает с версией БД, необходимой для ПО(" + DBVersion + "). Приложение будет закрыто.");
                        if (App.Current != null)
                            App.Current.Shutdown();
                    }
                }
#if DEBUG
                DebugLog.Dump("CheckDBVersion() -> key ok\r\n");
#endif
            }
        }

        /// <summary>
        /// Обновление БД с версии 1.0.0.0 до 1.0.0.1
        /// </summary>
        private void UpdateDBFrom1000To1001()
        {
            try
            {
                new SqlCeCommand("CREATE TABLE [Audio]([ID] [uniqueidentifier] UNIQUE ROWGUIDCOL NOT NULL,[ListID] [uniqueidentifier] NOT NULL,[Lyrics] [ntext] NULL,[Name] [ntext] NOT NULL, [DownloadID] [uniqueidentifier] NOT NULL,CONSTRAINT [PK_Audio] PRIMARY KEY ([ID] ) )", Connection).ExecuteNonQuery();
                new SqlCeCommand("UPDATE [SettingsTable] SET [value]='1.0.0.1' WHERE [key] LIKE 'DBVersion'", Connection).ExecuteNonQuery();
            }
            catch { }
            CheckDBVersion();
        }

        /// <summary>
        /// Обновление БД с версии 1.0.0.1 до 1.0.0.2
        /// </summary>
        private void UpdateDBFrom1001To1002()
        {
            try
            {
                new SqlCeCommand("ALTER TABLE [Audio] ADD [Length] int",Connection).ExecuteNonQuery();
                new SqlCeCommand("UPDATE [SettingsTable] SET [value]='1.0.0.2' WHERE [key] LIKE 'DBVersion'", Connection).ExecuteNonQuery();
            }
            catch { }
            CheckDBVersion();
        }

        /// <summary>
        /// Строка соединения
        /// </summary>
        static string connectionString
        {
            get
            {
                string S = "Data Source=";
                S += System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
                S += "\\database.sdf";
                return S;
                //return System.Reflection.Assembly.GetExecutingAssembly().Location + "Data Source=database.sdf";
            }
        }

        /// <summary>
        /// Экземпляр соединения
        /// </summary>
        private SqlCeConnection _connection;

        /// <summary>
        /// Свойство соединения, реализующее ленивую инициализацию
        /// </summary>
        SqlCeConnection Connection
        {
            get
            {
#if DEBUG
                DebugLog.Dump("SqlCEAPI.Connection._get\r\n");
#endif
                if (_connection == null)
                {
#if DEBUG
                    DebugLog.Dump("SqlCeAPI.Connection -> _conn is null");
#endif
                    _connection = new SqlCeConnection(connectionString);
#if DEBUG
                    DebugLog.Dump("SqlCeAPI.Connection -> created connection");
#endif
                    try
                    {
                        _connection.Open();
                    }
                    catch
#if DEBUG               
                        (Exception e)
#endif
                    {
#if DEBUG
                        DebugLog.DebugException(e);
#endif
                        App.Current.Shutdown();
                    }
#if DEBUG
                    DebugLog.Dump("SqlCeAPI.Connection -> _conn opened");
#endif
                }
#if DEBUG
                DebugLog.Dump("SqlCeAPI.Connection -> return _conn");
#endif
                return _connection;
            }
        }

        /// <summary>
        /// Получение DataSet-а по запросу
        /// </summary>
        /// <param name="query">Запрос T-SQL</param>
        /// <returns>Полученный DataSet</returns>
        private DataSet GetDataSet(string query)
        {
            var con = Connection;
            var ds = new DataSet();
            ds.Locale = CultureInfo.CurrentCulture;
            try
            {
                var da = new SqlCeDataAdapter(query, con);
                da.Fill(ds);
            }
            catch(Exception e)
            {
                DebugLog.DebugException(e);
                return null;
            }
            return ds;
        }

        /// <summary>
        /// Получение объектов из таблицы
        /// </summary>
        /// <param name="Table">Название таблицы</param>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="isExact">Точное ли время</param>
        /// <param name="Columns">Названия столбцов таблицы</param>
        /// <returns>Массив объектов</returns>
        private object[] GetObjects(string Table, int HumanID, string PropertyName, DateTime ShowTime,bool isExact,params string[] Columns)
        {
            var Key = new CombinedKey()
            {
                HumanID = HumanID,
                When = ShowTime,
                Table = Table,
                PropertyName = PropertyName
            };
            object[] X = null;
            if (_sqlLocalCache.Keys.Contains(Key))
                return _sqlLocalCache[Key];
            lock (_sqlLocalCache)
            {
                var query = "SELECT " + string.Join(",", Columns);
                query += ",[PropertyName] FROM [" + Table + "] T0, ";
                query += "(SELECT Max([When])AS [C1],[PropertyName] AS [C2] FROM ["+Table+"] WHERE [HumanID] = '" + HumanID + "' AND [When] " + (isExact ? "" : "<") + "= '" + ShowTime.ToString(System.Globalization.CultureInfo.InvariantCulture) + "' GROUP BY [PropertyName])";
                query += "T1 WHERE [When]=[C1] AND [PropertyName]=[C2] AND [HumanID] = '" + HumanID + "' ";
                query += "ORDER BY [When] DESC";
#if DEBUG
                DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
                var ds = GetDataSet(query);
                if (ds != null && ds.Tables.Count > 0)
                {
                    foreach (DataRow v in ds.Tables[0].Rows)
                    {
                        _sqlLocalCache[new CombinedKey()
                        {
                            HumanID = HumanID,
                            When = ShowTime,
                            Table = Table,
                            PropertyName = (string)v.ItemArray[v.ItemArray.Length - 1]
                        }] = v.ItemArray.Take(v.ItemArray.Length - 1).Select(s => s is string ? s.ToString().UndoEscapeString() : s).ToArray();
                    }
                    if (_sqlLocalCache.Keys.Contains(Key))
                        X = _sqlLocalCache[Key];
                }
            }
            return X;
        }

        /// <summary>
        /// Ключ для локального кэша
        /// </summary>
        struct CombinedKey
        {
            public override bool Equals(object obj)
            {
                if (obj is CombinedKey)
                {
                    var x = (CombinedKey)obj;
                    return (x.HumanID == this.HumanID && x.PropertyName == this.PropertyName && x.Table == this.Table && x.When == this.When);
                }
                return base.Equals(obj);
            }
            public override int GetHashCode()
            {
                return base.GetHashCode() ^ Table.GetHashCode() ^ When.GetHashCode() ^ HumanID.GetHashCode() ^ PropertyName.GetHashCode();
            }
            public string Table;
            public DateTime When;
            public int HumanID;
            public string PropertyName;
        }

        /// <summary>
        /// Локальный кэш таблиц БД
        /// </summary>
        private static Dictionary<CombinedKey, object[]> _sqlLocalCache;

        /// <summary>
        /// Получает было ли загружено свойство в БД.
        /// </summary>
        /// <param name="HumanID">ID человека</param>
        /// <param name="PropertyName">Название свойства</param>
        /// <returns>Было ли загружено данное свойство данного человека</returns>
        public bool IsPropertyLoaded(int humanId, string propertyName)
        {
            var Key = new CombinedKey()
            {
                Table = "HumanoidPropertyLoaded",
                HumanID = humanId,
                PropertyName = propertyName,
                When = DateTime.MinValue
            };
            bool X = false;
            lock (_sqlLocalCache)
            {
                if (_sqlLocalCache.Keys.Where(v=>v.HumanID == humanId && v.PropertyName == propertyName&& v.Table == "HumanoidPropertyLoaded").Count()>0)
                    return true;
                if (_sqlLocalCache.Keys.Any(v => v.Table == "HumanoidPropertyLoaded"))
                    return false;
                var query = "SELECT DISTINCT PropertyName,HumanID FROM [HumanoidPropertyLoaded]";// WHERE [HumanID] = '" + HumanID + "'";// AND [PropertyName] = '"+PropertyName+"'";
#if DEBUG
                DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
                var ds = GetDataSet(query);
                if (ds != null && ds.Tables.Count > 0)
                {
                    foreach (DataRow v in ds.Tables[0].Rows)
                    {
                        _sqlLocalCache[new CombinedKey()
                        {
                            HumanID = (int)v.ItemArray[1],
                            When = DateTime.MinValue,
                            Table = "HumanoidPropertyLoaded",
                            PropertyName = (string)v.ItemArray[0]
                        }] = null;
                    }
                    if (_sqlLocalCache.Keys.Contains(Key))
                        X = true;
                }
            }
            return X;
        }

        /// <summary>
        /// Устанавливает что определенное свойство было сохранено в БД.
        /// </summary>
        /// <param name="HumanID">ИД человека</param>
        /// <param name="PropertyName">название свойства</param>
        public void PropertyLoaded(int humanId, string propertyName)
        {
            if (!IsPropertyLoaded(humanId, propertyName))
            {
                var query = "INSERT INTO [HumanoidPropertyLoaded] ([HumanID],[PropertyName]) VALUES ('"+humanId+"','"+propertyName+"')";
                lock (_sqlLocalCache)
                {
                    _sqlLocalCache[new CombinedKey()
                    {
                        HumanID = humanId,
                        When = DateTime.MinValue,
                        Table = "HumanoidPropertyLoaded",
                        PropertyName = propertyName
                    }] = null;
                }
#if DEBUG
                DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
                SqlCeCommand cmd = new SqlCeCommand(query, Connection);
                cmd.ExecuteNonQuery();
            }
        }

        /// <summary>
        /// Получает объект из БД
        /// </summary>
        /// <param name="Table">Название таблицы</param>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="isExact">Точное ли время</param>
        private object GetObject(string Table, int HumanID, string PropertyName, DateTime ShowTime, bool isExact)
        {
            var X = GetObjects(Table, HumanID, PropertyName, ShowTime, isExact, "Value");
            if (X != null) return X[0];
            else return null;
        }

        /// <summary>
        /// Получает объект типа Downloadable из БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="isExact">Точное ли время</param>
        public Downloadable GetDownloadable(int humanId, string propertyName, DateTime showTime, bool isExact)
        {
            var X = GetObjects("HumanoidDownloadables", humanId, propertyName, showTime, isExact, "URI", "LocalFilename");
            if (X == null) return null;
            
            return new Downloadable()
            {
                LocalFileName = (X[1] is DBNull)?null:((string)X[1]).UndoEscapeString(),
                Uri = new Uri(((string)X[0]).UndoEscapeString())
            };
        }

        /// <summary>
        /// Сохраняет в БД объект типа Downloadable
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="Value">Значение которое надо сохранить</param>
        public void SetDownloadable(int humanId,string propertyName, Downloadable value, DateTime showTime)
        {
            SetObjects("HumanoidDownloadables", humanId, propertyName, new string[2] { value.InternetUri.AbsoluteUri.EscapeString(), (value.LocalFileName.EscapeString()??"") }, showTime, "URI", "LocalFilename");
        }

        /// <summary>
        /// Получает двойную строку из БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="isExact">Точное ли время</param>
        public string[] GetDualStrings(int humanId, string propertyName, DateTime showTime, bool isExact)
        {
            var X = GetObjects("HumanoidDualStrings", humanId, propertyName, showTime, isExact, "Value0", "Value1");
            if (X == null) return null;
            else return new string[2] { (string)X[0], (string)X[1] };
        }

        /// <summary>
        /// Сохраняет двойную строку в БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="Value">Значение которое надо сохранить</param>
        public void SetDualStrings(int humanId, string propertyName, string[] value, DateTime showTime)
        {
            if (value == null) value = new string[] { "", "" };
            SetObjects("HumanoidDualStrings", humanId, propertyName, new string[2] { value[0].EscapeString(), value[1].EscapeString() }, showTime, "Value0", "Value1");
        }

        /// <summary>
        /// Получает Relation из БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="isExact">Точное ли время</param>
        public Relation GetRelation(int humanId, string propertyName, DateTime showTime, bool isExact)
        {
            var Y = GetObjects("HumanoidRelations", humanId, propertyName, showTime, isExact, "TypeOfRelation", "WithID");
            if (Y == null) return null;
            else return new Relation()
            {
                RelationType = (RelationType)(int)Y[0],
                With = (Y[1] is DBNull)?null:Human.GetHuman((int)Y[1])
            };
        }

        /// <summary>
        /// Сохраяет Relation в БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="Value">Значение которое надо сохранить</param>
        public void SetRelation(int humanId, string propertyName, Relation value, DateTime showTime)
        {
            if (value != null)
                SetObjects("HumanoidRelations", humanId, propertyName, new string[2] 
            {
                ((int)value.RelationType).ToString(CultureInfo.InvariantCulture), 
                value.With == null?null:value.With.Id.ToString(CultureInfo.InvariantCulture) 
            }, showTime, "TypeOfRelation", "WithID");
        }

        /// <summary>
        /// Получает список LowEducation из БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="isExact">Точное ли время</param>
        public List<LowEducation> GetListLowEducation(int humanId, string propertyName, DateTime showTime, bool isExact)
        {
            var X = GetObjects("HumanoidLowEducations", humanId, propertyName, showTime, isExact, "ID");
            if (X == null) return null;
            var Y = (Guid)X[0];
            if (Y == null) return null;
            var query = "SELECT [Country], [City], [School], [Begin], [End], [Finish],[Class],[Speciality] FROM [LowEducation] WHERE [ID] = '" + Y + "' ";
#if DEBUG
            DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
            var ds = GetDataSet(query);
            List<LowEducation> L = new List<LowEducation>();
            if (ds != null && ds.Tables[0].Rows.Count > 0)
                foreach (DataRow w in ds.Tables[0].Rows)
                    L.Add(new LowEducation()
                            {
                                Country = ((string)w[0]).UndoEscapeString(),
                                City = ((string)w[1]).UndoEscapeString(),
                                School = ((string)w[2]).UndoEscapeString(),
                                Begin = (DateTime)(w[3] ?? DateTime.MinValue),
                                End = (DateTime)(w[4] ?? DateTime.MinValue),
                                Finish = (DateTime)(w[5] ?? DateTime.MinValue),
                                Class = ((string)w[6]).UndoEscapeString(),
                                Speciality = ((string)w[7]).UndoEscapeString()
                            }
                        );
            return L;
        }

        /// <summary>
        /// Сохраняет список LowEducation в БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="Value">Значение которое надо сохранить</param>
        public void SetListLowEducation(int humanId, string propertyName, List<LowEducation> value, DateTime showTime)
        {
            var g = Guid.NewGuid();
            var query = "INSERT INTO [HumanoidLowEducations] ( [ID], [PropertyName], [When], [HumanID] ) VALUES ( ";
            query += "'" + g + "', ";
            query += "'" + propertyName + "', ";
            query += "'" + showTime.ToString(System.Globalization.CultureInfo.InvariantCulture) + "', ";
            query += "'" + humanId + "')";
#if DEBUG
            DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
            var con = Connection;
            try
            {
                SqlCeCommand cmd = new SqlCeCommand(query, con);
                cmd.ExecuteNonQuery();
                foreach (var v in value)
                {
                    string q = "INSERT INTO [LowEducation] ( [realID], [ID]";
                    q += ", [Country]";
                    q += ", [City]";
                    q += ", [School]";
                    q += ", [Begin]";
                    q += ", [End]";
                    q += ", [Finishч]";
                    q += ", [Class]";
                    q += ", [Speciality]";
                    q += ") VALUES ( '" + Guid.NewGuid() + "' , '" + g;
                    q += ", '" + v.Country.EscapeString() + "'";
                    q += ", '" + v.City.EscapeString() + "'";
                    q += ", '" + v.School.EscapeString() + "'";
                    q += ", '" + v.Begin.ToString(System.Globalization.CultureInfo.InvariantCulture) + "'";
                    q += ", '" + v.End.ToString(System.Globalization.CultureInfo.InvariantCulture) + "'";
                    q += ", '" + v.Finish.ToString(System.Globalization.CultureInfo.InvariantCulture) + "'";
                    q += ", '" + v.Class.EscapeString() + "'";
                    q += ", '" + v.Speciality.EscapeString() + "'";
                    q += "' )";
#if DEBUG
                    DebugLog.Dump(DateTime.Now + "->" + q + "\r\n");
#endif
                    SqlCeCommand cm = new SqlCeCommand(q, con);
                    cm.ExecuteNonQuery();
                }
            }
            catch(Exception e)
            {
                DebugLog.DebugException(e);
            }
        }

        /// <summary>
        /// Загружает из БД список аудиозаписей пользователя
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="isExact">Точное ли время</param>
        public IEnumerable<Audio> GetAudioList(int humanId, string propertyName, DateTime showTime, bool isExact)
        {
            var X = GetObjects("HumanoidListInts", humanId, propertyName, showTime, isExact, "ID");
            if (X == null) return null;
            var Y = (Guid)X[0];
            if (Y == null) return null;
            var query = "SELECT [Lyrics],[Name],[DownloadID],[Length] FROM [Audio] WHERE [ListID] = '" + Y + "' ";
#if DEBUG
            DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
            var ds = GetDataSet(query);
            List<Audio> L = new List<Audio>();
            if (ds != null && ds.Tables[0].Rows.Count > 0)
                foreach (DataRow w in ds.Tables[0].Rows)
                {
                    string URI = (string)new SqlCeCommand("SELECT [URI] FROM [HumanoidDownloadables] WHERE [ID]='" + w[2] + "'", Connection).ExecuteScalar();
                    string LocalFilename = (string)new SqlCeCommand("SELECT [LocalFilename] FROM [HumanoidDownloadables] WHERE [ID]='" + w[2] + "'", Connection).ExecuteScalar();
                    L.Add(new Audio()
                    {
                        Lyrics = ((string)w[0]).UndoEscapeString(),
                        Name = ((string)w[1]).UndoEscapeString(),
                        File = new Downloadable()
                        {
                            Uri = new Uri(URI.UndoEscapeString(), UriKind.Absolute),
                            LocalFileName = LocalFilename.UndoEscapeString()
                        },
                        MaximumProgress = (int)w[3]
                    });
                }
            return L.ToList();
        }

        /// <summary>
        /// Сохраняет список аудиозаписей пользователя в БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="value">Значение которое надо сохранить</param>
        public void SetAudioList(int humanId, string propertyName, IEnumerable<Audio> value, DateTime showTime)
        {
            var g = Guid.NewGuid();
            var query = "INSERT INTO [HumanoidListInts] ( [ID], [PropertyName], [When], [HumanID] ) VALUES ( ";
            query += "'" + g + "', ";
            query += "'" + propertyName + "', ";
            query += "'" + showTime.ToString(System.Globalization.CultureInfo.InvariantCulture) + "', ";
            query += "'" + humanId + "')";
#if DEBUG
            DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
            var con = Connection;
            try
            {
                SqlCeCommand cmd = new SqlCeCommand(query, con);
                cmd.ExecuteNonQuery();
                foreach (var v in value)
                {
                    int count = (int)new SqlCeCommand("SELECT COUNT(*) FROM [HumanoidDownloadables] WHERE [URI] LIKE '"+v.File.InternetUri.AbsoluteUri.EscapeString()+"'", Connection).ExecuteScalar();
                    if (count == 0)
                        SetDownloadable(0, "", v.File, DateTime.Now);
                    var X = new SqlCeCommand("SELECT [ID] FROM [HumanoidDownloadables] WHERE [URI] LIKE '" + v.File.InternetUri.AbsoluteUri.EscapeString() + "'", Connection).ExecuteScalar();
                    Guid ID = (Guid)X;
                    string q = "INSERT INTO [Audio] ( [listID], [Lyrics], [Name], [DownloadID], [ID], [Length] ) VALUES ( '"
                        + g + "' , '" + v.Lyrics.EscapeString() + "' , '" + v.Name.EscapeString() + "' , '" + ID + "' , '" + Guid.NewGuid() + "', '" + v.MaximumProgress + "' )";

#if DEBUG
                    DebugLog.Dump(DateTime.Now + "->" + q + "\r\n");
#endif
                    SqlCeCommand cm = new SqlCeCommand(q, con);
                    cm.ExecuteNonQuery();
                }
            }
            catch (Exception e)
            {
                DebugLog.DebugException(e);
            }
        }

        /// <summary>
        /// Получает список HighEducation из БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="isExact">Точное ли время</param>
        public List<HighEducation> GetListHighEducation(int humanId, string propertyName, DateTime showTime, bool isExact)
        {
            var X = GetObjects("HumanoidHighEducation", humanId, propertyName, showTime, isExact, "ID");
            if (X == null) return null;
            var Y = (Guid)X[0];
            if (Y == null) return null;
            var query = "SELECT [University], [Faculty], [Cathedral], [EducationForm], [EducationStatus], [Finish] FROM [HighEducations] WHERE [ID] = '" + Y + "' ";
#if DEBUG
            DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
            var ds = GetDataSet(query);
            List<HighEducation> L = new List<HighEducation>();
            if (ds != null && ds.Tables[0].Rows.Count > 0)
                foreach (DataRow w in ds.Tables[0].Rows)
                    L.Add(new HighEducation()
                            {
                                University = ((string)w[0]).UndoEscapeString(),
                                Faculty = ((string)w[1]).UndoEscapeString(),
                                Cathedral = ((string)w[2]).UndoEscapeString(),
                                Form = (EducationForm)(int)(w[3] ?? 0),
                                Status = (EducationStatus)(int)(w[4] ?? 0),
                                Finish = (DateTime)((w[5] is DBNull?null:w[5]) ?? DateTime.MinValue)
                            }
                        );
            return L;
        }

        /// <summary>
        /// Сохраняет список HighEducation в БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="Value">Значение которое надо сохранить</param>
        public void SetListHighEducation(int humanId, string propertyName, List<HighEducation> value, DateTime showTime)
        {
            var g = Guid.NewGuid();
            var query = "INSERT INTO [HumanoidHighEducation] ( [ID], [PropertyName], [When], [HumanID] ) VALUES ( ";
            query += "'" + g + "', ";
            query += "'" + propertyName + "', ";
            query += "'" + showTime.ToString(System.Globalization.CultureInfo.InvariantCulture) + "', ";
            query += "'" + humanId + "')";
#if DEBUG
            DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
            var con = Connection;
            try
            {
                SqlCeCommand cmd = new SqlCeCommand(query, con);
                cmd.ExecuteNonQuery();
                foreach (var v in value)
                {
                    string q = "INSERT INTO [HighEducations] ( [realID], [ID]";
                    q += ", [University]";
                    q += ", [Faculty]";
                    q += ", [Cathedral]";
                    q += ", [Finish]";
                    q += ", [EducationForm]";
                    q += ", [EducationStatus]";
                    q += ") VALUES ( '" + Guid.NewGuid() + "' , '" + g;
                    q += ", '" + v.University.EscapeString() + "'";
                    q += ", '" + v.Faculty.EscapeString() + "'";
                    q += ", '" + v.Cathedral.EscapeString() + "'";
                    q += ", '"+v.Finish.ToString(System.Globalization.CultureInfo.InvariantCulture)+"'";
                    q += ", '"+(int)v.Form+"'";
                    q += ", '"+(int)v.Status+"'";
                    q += "' )";
#if DEBUG
                    DebugLog.Dump(DateTime.Now + "->" + q + "\r\n");
#endif
                    SqlCeCommand cm = new SqlCeCommand(q, con);
                    cm.ExecuteNonQuery();
                }
            }
            catch(Exception e)
            {
                DebugLog.DebugException(e);
            }
        }

        /// <summary>
        /// Локальный кэш таблицы модификаций 
        /// </summary>
        private Dictionary<int, List<DateTime>> _modifiesCache;

        /// <summary>
        /// Получает даты изменения свойств у человека
        /// </summary>
        /// <param name="HumanID">ID человека</param>
        /// <returns>Список дат</returns>
        public IEnumerable<DateTime> GetModifies(int humanId)
        {
            if (_modifiesCache.Keys.Contains(humanId)) return _modifiesCache[humanId];

            lock (_modifiesCache)
            {
                List<TwoColumnKey<int,DateTime>> list = new List<TwoColumnKey<int,DateTime>>();
                string[] tables = { "HumanoidBools", "HumanoidDateTime", "HumanoidDownloadables", "HumanoidDualStrings", "HumanoidHighEducation", "HumanoidInts", "HumanoidListInts", "HumanoidLowEducations", "HumanoidRelations", "HumanoidStrings" };
                var query = String.Join(" UNION ", tables.Select(v => "SELECT [When],[HumanID] FROM [" + v + "]").ToArray());
#if DEBUG
                DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
                var ds = GetDataSet(query);
                if (ds != null && ds.Tables[0].Rows.Count > 0)
                    foreach (DataRow w in ds.Tables[0].Rows)
                        list.Add(new TwoColumnKey<int, DateTime>() { Key1 = (int)w[1], Key2 = (DateTime)w[0] });
                foreach (var v in list.GroupBy(v => v.Key1))
                {
                    _modifiesCache[v.Key] = v.Select(q => q.Key2).ToList();
                }
            }
            if (_modifiesCache.Keys.Contains(humanId)) return _modifiesCache[humanId];
            else return new List<DateTime>();
        }

        /// <summary>
        /// Сохраняет список друзей пользователя в БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="Value">Значение которое надо сохранить</param>
        public void SetFriends(int humanId, string propertyName, IEnumerable<Human> value, DateTime showTime)
        {
            var g = Guid.NewGuid();
            var query = "INSERT INTO [HumanoidListInts] ( [ID], [PropertyName], [When], [HumanID] ) VALUES ( ";
            query += "'" + g + "', ";
            query += "'" + propertyName + "', ";
            query += "'" + showTime.ToString(System.Globalization.CultureInfo.InvariantCulture) + "', ";
            query += "'" + humanId + "')";
#if DEBUG
            DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
            var con = Connection;
            try
            {
                SqlCeCommand cmd = new SqlCeCommand(query, con);
                cmd.ExecuteNonQuery();
                foreach (var v in value)
                {
                    string q = "INSERT INTO [ListInts] ( [realID], [Value], [ID] ) VALUES ( '"+Guid.NewGuid()+"' , '"+v.Id+"' , '"+g+"' )";
#if DEBUG
                    DebugLog.Dump(DateTime.Now + "->" + q + "\r\n");
#endif
                    SqlCeCommand cm = new SqlCeCommand(q, con);
                    cm.ExecuteNonQuery();
                }
            }
            catch(Exception e)
            {
                DebugLog.DebugException(e);
            }
        }

        /// <summary>
        /// Получает список друзей пользователя из БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="isExact">Точное ли время</param>
        public IEnumerable<Human> GetFriends(int humanId, string propertyName, DateTime showTime, bool isExact)
        {
            var X = GetObjects("HumanoidListInts", humanId, propertyName, showTime, isExact, "ID");
            if (X == null) return null;
            var Y = (Guid)X[0];
            if (Y == null) return null;
            var query = "SELECT Value FROM [ListInts] WHERE [ID] = '" + Y + "' ";
#if DEBUG
            DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
            var ds = GetDataSet(query);
            List<int> L = new List<int>();
            if (ds != null && ds.Tables[0].Rows.Count > 0)
                foreach (DataRow w in ds.Tables[0].Rows)
                    L.Add((int)w[0]);
            return L.Select(v => Human.GetHuman(v,false)).OrderBy(v=>v.Id).ToList();
        }

        /// <summary>
        /// Получает целочисленное свойство из БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="isExact">Точное ли время</param>
        public int GetInt(int humanId, string propertyName, DateTime showTime, bool isExact)
        {
            return (int)(GetObject("HumanoidInts", humanId, propertyName, showTime, isExact)??int.MinValue);
        }

        /// <summary>
        /// Сохраняет целочисленное свойство в БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="Value">Значение которое надо сохранить</param>
        public void SetInt(int humanId, string propertyName, int value, DateTime showTime)
        {
            SetObject("HumanoidInts", humanId, propertyName, value.ToString(CultureInfo.InvariantCulture), showTime);
        }

        /// <summary>
        /// Получает дату/время из БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="isExact">Точное ли время</param>
        public DateTime GetDateTime(int humanID, string propertyName, DateTime showTime, bool isExact)
        {
            return (DateTime)(GetObject("HumanoidDateTime", humanID, propertyName, showTime, isExact)??DateTime.MinValue);
        }

        /// <summary>
        /// Сохраняет дату/время в БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="Value">Значение которое надо сохранить</param>
        public void SetDateTime(int humanID, string propertyName, DateTime value, DateTime showTime)
        {
            DateTime d = value;
            if (d == default(DateTime)) d = DateTime.Now;
            SetObject("HumanoidDateTime", humanID, propertyName, d.ToString(System.Globalization.CultureInfo.InvariantCulture), showTime);
        }



        /// <summary>
        /// Получает строку из БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="isExact">Точное ли время</param>
        public string GetString(int humanId, string propertyName, DateTime showTime, bool isExact)
        {
            return (string)GetObject("HumanoidStrings", humanId, propertyName, showTime, isExact);
        }

        /// <summary>
        /// Получает булевое из БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="isExact">Точное ли время</param>
        public bool GetBool(int humanId, string propertyName, DateTime showTime, bool isExact)
        {
            var X = GetObject("HumanoidBools", humanId, propertyName, showTime, isExact);
            if (X == null) return false;
            else return (bool)X;
        }

        /// <summary>
        /// Сохранение объектов в БД
        /// </summary>
        /// <param name="Table">Название таблицы</param>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="Value">Значение которое надо сохранить</param>
        /// <param name="Columns">Названия столбцов</param>
        private void SetObjects(string Table, int HumanID, string PropertyName, string[] Value, DateTime ShowTime, params string[] Columns)
        {
            var query = "INSERT INTO [" + Table + "] ( [ID], [PropertyName], [When], [HumanID], "+String.Join(", ",Columns)+") VALUES ( ";
            query += "'" + Guid.NewGuid() + "', ";
            query += "'" + PropertyName + "', ";
            var DateTimeStr = "NULL";
            if (ShowTime != default(DateTime)) DateTimeStr = "'"+ShowTime.ToString(System.Globalization.CultureInfo.InvariantCulture)+"'";
            query += DateTimeStr + ", ";
            query += "'" + HumanID + "', ";
            query += "" + String.Join(",", Value.Select(s=>s==null?"NULL":"'"+s.EscapeString()+"'").ToArray()) + ")";
#if DEBUG
            DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif

            var con = Connection;
            try
            {
                SqlCeCommand cmd = new SqlCeCommand(query, con);
                cmd.ExecuteNonQuery();
                if (!_modifiesCache.Keys.Contains(HumanID)) _modifiesCache[HumanID] = new List<DateTime>();
                if (!_modifiesCache[HumanID].Contains(ShowTime)) _modifiesCache[HumanID].Add(ShowTime);
            }
            catch(Exception e)
            {
                DebugLog.DebugException(e);
            }
        }

        /// <summary>
        /// Сохранение объекта
        /// </summary>
        /// <param name="Table">Название таблицы</param>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="Value">Значение которое надо сохранить</param>
        private void SetObject(string Table, int HumanID, string PropertyName, string Value, DateTime ShowTime)
        {
            SetObjects(Table, HumanID, PropertyName, new string[1] { Value }, ShowTime, "Value");
        }

        /// <summary>
        /// Сохраняет строку в БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="Value">Значение которое надо сохранить</param>
        public void SetString(int humanId, string propertyName, string value, DateTime showTime)
        {
            SetObject("HumanoidStrings", humanId, propertyName, value, showTime);
        }

        /// <summary>
        /// Сохраняет булевое в БД
        /// </summary>
        /// <param name="HumanID">Номер человека в ВКонтакте</param>
        /// <param name="PropertyName">Имя свойства которое надо получить</param>
        /// <param name="ShowTime">Время. Возвращается значение свойства, соответствующее этому слепку времени</param>
        /// <param name="Value">Значение которое надо сохранить</param>
        public void SetBool(int humanId, string propertyName, bool value, DateTime showTime)
        {
            SetObject("HumanoidBools", humanId, propertyName, value ? "1" : "0", showTime);
        }

        /// <summary>
        /// Составной ключ из двух элементов для локального кэша, шаблонный. Используется в кэше времен пользователей.
        /// </summary>
        struct TwoColumnKey<T1,T2>
        {
            public T1 Key1;
            public T2 Key2;
        }

        /// <summary>
        /// Локальный кэш таблицы модификаций
        /// </summary>
        private Dictionary<TwoColumnKey<int, string>, DateTime?> _userTimeCache;

        /// <summary>
        /// Локальный кэш указывающий кэшированность заданного столбца таблицы времен пользователя
        /// </summary>
        private List<string> _userTimeLoaded;

        /// <summary>
        /// Получение времени пользователя из БД
        /// </summary>
        /// <param name="UserID">ID пользователя</param>
        /// <param name="Column">Название свойства времени</param>
        /// <returns></returns>
        public DateTime? GetUserTime(int humanId, string column)
        {
            var Key = new TwoColumnKey<int,string>() { Key1 = humanId, Key2 = column };
            lock (_userTimeCache)
            {
                if (!_userTimeLoaded.Contains(column))
                {
                    var query = "SELECT [" + column + "],[ID] FROM [Humanoids]";
#if DEBUG
                    DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
                    var ds = GetDataSet(query);
                    if (ds != null)
                        if (ds.Tables != null && ds.Tables.Count > 0)
                            if (ds.Tables[0].Rows != null && ds.Tables[0].Rows.Count > 0)
                                foreach (DataRow v in ds.Tables[0].Rows)
                                {
                                    DateTime? value = null;
                                    if (v.ItemArray[0] is DateTime) value = (DateTime)v.ItemArray[0];
                                    _userTimeCache[new TwoColumnKey<int, string>() { Key1 = (int)v.ItemArray[1], Key2 = column }] = value;
                                }
                    _userTimeLoaded.Add(column);
                }

                if (_userTimeCache.Keys.Contains(Key)) return _userTimeCache[Key];
                else
                {
                    _userTimeCache[Key] = null;
                    return null;
                }
            }
        }

        /// <summary>
        /// "Прикосновение" к пользователю в БД. Если нету такого пользователя - добавляет в БД
        /// </summary>
        /// <param name="UserID"></param>
        /// <returns></returns>
        public bool TouchUser(int userId)
        {
            var con = Connection;
            var result = false;
            try
            {
                var query = "SELECT COUNT(*) FROM Humanoids WHERE ID='" + userId + "'";
                SqlCeCommand cmd = new SqlCeCommand(query, con);
#if DEBUG
                DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
                int count = (int)(cmd.ExecuteScalar()??0);
                if (count == 0)
                {
                    SqlCeCommand cmd1 = new SqlCeCommand("INSERT INTO Humanoids (ID) VALUES ( '" + userId + "' )", con);
                    cmd1.ExecuteNonQuery();
                    result = true;
                }
            }
            catch(Exception e)
            {
                DebugLog.DebugException(e);
            }
            return result;
        }

        public string GetDownloadableLocalFileNameByUrl(string url)
        {
            var query = "SELECT [LocalFilename] FROM [HumanoidDownloadables] WHERE URI LIKE '%" + url + "%'";
            string s = (string)new SqlCeCommand(query, Connection).ExecuteScalar();
            return s;
        }

        /// <summary>
        /// Обновляет тип Downloadable в БД, обновляя ссылку на локальный файл
        /// </summary>
        /// <param name="Url">Url скачиваемого </param>
        /// <param name="LocalFile">Локальный адрес</param>
        public void UpdateDownloadable(string url, string localFile)
        {
            var query = "UPDATE [HumanoidDownloadables] SET [LocalFilename] = '" + localFile + "' WHERE URI LIKE '" + url + "'";
            try
            {
                int c = (int)new SqlCeCommand("SELECT COUNT(*) FROM [HumanoidDownloadables] WHERE URI LIKE '%" + url + "%'", Connection).ExecuteScalar();
                if (c == 0)
                {
                    var q = "INSERT INTO HumanoidDownloadables ([ID],[HumanID],[PropertyName],[WHEN],[URI],[LocalFileName]) VALUES ('" + Guid.NewGuid() + "',0,'','" + DateTime.Now.ToString(System.Globalization.CultureInfo.InvariantCulture) + "','" + url + "','" + localFile + "')";
                    new SqlCeCommand(q,Connection).ExecuteNonQuery();
#if DEBUG
                    DebugLog.Dump(DateTime.Now + "->" + q + "\r\n");
#endif
                }
                new SqlCeCommand(query, Connection).ExecuteNonQuery();
#if DEBUG
                DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
            }
            catch(Exception e)
            {
                DebugLog.DebugException(e);
            }
        }

        /// <summary>
        /// Сохраняет время пользователя в БД
        /// </summary>
        /// <param name="NewTime">Новое время для сохранения</param>
        /// <param name="ColumnName">Имя столбца в таблице</param>
        /// <param name="UserID">ID пользователя</param>
        public void SetUserTime(DateTime? newTime,string columnName,int userId)
        {
            var query = "UPDATE [Humanoids] SET [" + columnName + "]=";
            if (newTime != null && newTime.Value != DateTime.MinValue)
                query += "'"+newTime.Value.ToString(System.Globalization.CultureInfo.InvariantCulture)+"'";
            else
                query += "NULL";
            query += " WHERE ID='" + userId + "'";
#if DEBUG
            DebugLog.Dump(DateTime.Now + "->" + query + "\r\n");
#endif
            try
            {
                SqlCeCommand c = new SqlCeCommand(query, Connection);
                c.ExecuteNonQuery();
                _userTimeCache[new TwoColumnKey<int,string> { Key2 = columnName, Key1 = userId }] = newTime;
            }
            catch(Exception e)
            {
                DebugLog.DebugException(e);
            }
        }

    }
}
